#!/usr/bin/env python
import unittest
from types import MethodType
from utils.misc.functional import getArgSpec


class SignalBase(object):
    class Signals:
        """
        Default signals:
         'destroyed' and 'terminated' (you can connect to them)
        Signal objects. Possible syntax:
         class Start: pass
         class Stop: pass
        or any kind of hashable object (eg. strings)
        """
        pass

    def __init__(self):
        self.senders = {}
        self.receivers = {}
        self.destructed = False

    def __calculateReceiver(self, slot):
        if type(slot) == MethodType:
            return slot.__self__
        else:
            return None

    def connect(self, signals, slots, staticParams=None, nameBinds=None):
        """
        Connects slot to signal
         signals: signals to witch each slot will be connected
         slots: functions that will be called when signals emits (see emit method)
         staticParams: dictionary of parameters that statically passed into slot function
         nameBinds: dictionary that maps emits parameters and slot parameters {emitParam -> slotParam}
        """
        if not isinstance(signals, tuple):
            signals = (signals, )
        if not isinstance(slots, tuple):
            slots = (slots, )
        for signal in signals:
            for slot in slots:
                self.__doConnect(signal, slot, staticParams, nameBinds)

    def __doConnect(self, signal, slot, staticParams, nameBinds):
        receiver = self.__calculateReceiver(slot)
        receivers = self.receivers.setdefault(signal, {})
        slots = receivers.setdefault(receiver, {})
        slots[slot] = (staticParams, nameBinds, getArgSpec(slot))
        if receiver and hasattr(receiver, 'senders'):
            if self in receiver.senders:
                receiver.senders[self] += 1
            else:
                receiver.senders[self] = 1


    def disconnect(self, signal, slot):
        """
        Disconnect slot from signal
         signal: signal described in Signals section (if slot not connected to that signal nothing will be done)
         slot: function that will be disconnected from specified signal
        """
        receiver = self.__calculateReceiver(slot)
        receivers = self.receivers.get(signal, None)
        if receivers:
            slots = receivers.get(receiver, None)
            if slots:
                slots.pop(slot)
                if not slots:
                    receivers.pop(receiver)
                    if receiver and hasattr(receiver, 'senders'):
                        receiver.senders[self] -= 1
                        if receiver.senders[self] <= 0:
                            receiver.senders.pop(self)
            if not receivers:
                self.receivers.pop(signal)

    def __disconnectReceiver(self, receiver):
        for signal, receivers in self.receivers.items():
            slots = receivers.get(receiver, None)
            if slots:
                slots.clear()
                receivers.pop(receiver)
                if not receivers:
                    self.receivers.pop(signal, None)

    def emit(self, signal, **kwargs):
        """
        Emits signal with key-word parameters
         signal: signal described in Signals section
         **kwargs: parameters for slot execution (non positional arguments)

        If slot has a 'sender' parameter, it will be field with sender object (self)
        """
        receivers = self.receivers.get(signal, None)
        if receivers:
            for slots in receivers.itervalues():
                for slot, tp in slots.iteritems():
                    argSpec = tp[2]
                    passAll = argSpec.keywords
                    argSpec = argSpec.args
                    params = {}
                    for key in kwargs:
                        if passAll or (key in argSpec):
                            params.setdefault(key, kwargs[key])
                    if tp[1]:
                        for targetName, originalName in tp[1].iteritems():
                            if targetName in argSpec:
                                if passAll or originalName == 'sender':
                                    params.setdefault(targetName, kwargs[originalName])
                    if tp[0]:
                        for key, value in tp[0].iteritems():
                            if passAll or key in argSpec:
                                params.setdefault(key, value)
                    if passAll or ('sender' in argSpec):
                        params.setdefault('sender', self)
                    slot(**params)

    def destructor(self):
        """
        After this class cannot receive any signals.
        """
        if self.destructed:
            return
        for sender in self.senders.keys():
            sender.__disconnectReceiver(self)
        self.senders = {}
        self.destructed = True
        self.emit('destroyed')

    def terminator(self):
        """
        After this class cannot emit signals.
        """
        self.emit('terminated')
        for receivers in self.receivers.values():
            for receiver in receivers.keys():
                if receiver and hasattr(receiver, 'senders') and self in receiver.senders:
                    receiver.senders.pop(self)
        self.receivers = {}

    def __del__(self):
        self.destructor()


class TestSignalBase(unittest.TestCase):
    def testEmit(self):
        class A(SignalBase):
            class Signals:
                class SA: pass

        class B(SignalBase):
            def __init__(self):
                SignalBase.__init__(self)
                self.counter = 0

            def c(self, value, value2):
                self.counter += value + value2

        a = A()
        b = B()
        a.connect(a.Signals.SA, b.c, dict(value2=3))
        a.emit(a.Signals.SA, value=7)
        a.emit(a.Signals.SA, value=10)
        b.destructor()
        a.emit(a.Signals.SA, value=4)
        self.assertEqual(b.counter, 23)


if __name__ == '__main__':
    testSuite = unittest.TestLoader().loadTestsFromTestCase(TestSignalBase)
    unittest.TextTestRunner(verbosity=2).run(testSuite)